Skip to content

Prototype manifest v2 Java SDK - #19

Draft
eunomie wants to merge 7 commits into
dagger:mainfrom
eunomie:java-sdk-manifest-v2-prototype-lead-c62ff1c1
Draft

Prototype manifest v2 Java SDK#19
eunomie wants to merge 7 commits into
dagger:mainfrom
eunomie:java-sdk-manifest-v2-prototype-lead-c62ff1c1

Conversation

@eunomie

@eunomie eunomie commented Sep 2, 2026

Copy link
Copy Markdown
Member

Purpose

This is an inspiration prototype for manifest v2, the Java counterpart to dagger/go-sdk#36. It is not meant to merge as-is. The review points are the code-reuse boundary and the shape of the generated entrypoint.

It targets the interface as specified in dagger/dagger#14038 (future/module-manifest-v2/spec.md, head 75c7772), not the earlier informal draft that go-sdk#36 was written against. Where the two disagree, this follows the specification and the design note says so.

Java is a smaller job than Go here, and that is the interesting part. Most of dagger/go-sdk#36 is relocation: the Go module analyzer lives in dagger/dagger, so the Go SDK has to port the analyzer, its source maps, pragmas, codecs and static dispatcher into the SDK repo before it can emit anything. Java has no such move to make — its analyzer is an annotation processor that already lives here and already runs at build time. So this branch is an unusually clean test of the v2 protocol change on its own.

Design

  • Give the annotation processor a second rendering backend: the same ModuleInfo and DaggerType that produce the runtime register() call now also render a Dang ModuleEntrypoint with types and call.
  • Replace the ambient currentFunctionCall entrypoint with an exported, static daggerDispatch, plus an engine-call argument mode on the generated main.
  • Put the stdin/stdout call protocol in hand-written SDK code (io.dagger.client.ModuleDispatcher) rather than generating a dispatch command per module, as Go must. It reads the request shape the specification documents: receiverValue and fnArgs as strings holding JSON text, decoded once.
  • Emit a manifest-v2 body: manifestVersion, name, and an [entrypoint] table with kind = "dang" — exactly the keys the engine accepts.
  • Keep this SDK loadable on manifest v1 and engine v1.0.0-beta.11.

The design note is in hack/designs/done/2026-09-02-manifest-v2-prototype.md. It includes the complete rendered entrypoint for a sample module, regenerated verbatim from the code.

What this says about the v2 interface

Four gaps, documented in full:

  1. call(...): JSON! has no structured error channel. A failure is a GraphQL error, so the fact of a failure and its message survive; what disappears is the typed Error value v1 builds from fnCall.returnError — for Java that includes stdout, stderr, cmd, exitCode and path from a failed withExec. The prototype writes those as a JSON envelope to stderr — explicitly not a mitigation, just a concrete record of what an error channel would have to carry. Telemetry initialisation goes the same way.
  2. Java has a main type; manifest v2 does not. The spec finds a module's entry object by looking for the type with a constructor and explicitly never compares names. Java's analyzer does the opposite: it records a constructor only on the @Object whose name matches the module. That satisfies the engine's current one-constructor limit for free, but it is a restriction v2 does not impose, and when the engine lifts the limit Java will not follow, because the constraint is in the analyzer.
  3. Name casing in call is specified — original names throughout. An earlier reading treated this as an undocumented assumption; the spec settles it, and Java's dispatcher already matched it.
  4. A module's own description has nowhere to go in types(): [TypeDef!]!.

Current limits

  • The manifest-v2 loader exists only on the Load modules through manifest v2 entrypoints dagger#14038 branch, so a generated entrypoint cannot be loaded normally.
  • The generated Dang references ModuleEntrypoint and Workspace.cwd, neither of which is in a release.
  • v2 generation is a separate generateV2 function, not wired into dagger generate, so no module commits a dead entrypoint.
  • The manifest is written to dagger-module.v2.toml, not dagger-module.toml: this SDK reads a module's schema by asking the engine to load it, so replacing the v1 manifest would make the module unloadable and generateV2 unrepeatable.

Validation

Same shape as dagger/go-sdk#36 — unit tests plus a generated fixture exercised directly, not a dagger call through a released engine.

  • dagger check: green.
  • packager:unit-tests: 28 tests across DaggerTypeDangTest, ModuleDispatcherTest and DangEntrypointRendererTest.
  • e-2-e:manifest-v-2-check: asserts the emitted Dang entrypoint (two fields, no main, the spec's call signature, every type including the module's main class, exactly one constructor) and the manifest (kind = "dang", no engineVersion).
  • e-2-e:manifest-v-2-dispatch-check: builds a fixture module's shaded jar and runs java -jar <jar> engine-call < request.json in a plain Maven container with no Dagger session, sending the request shape the specification documents and asserting the JSON result. This is a real execution of the generated dispatcher.

An inspiration prototype for Dagger manifest v2, the Java counterpart to
dagger/go-sdk#36. Records the problem, why Java isolates the protocol change
better than Go does, the proposed one-analyzer/two-backends approach, the four
known gaps in the v2 interface (chief among them the missing error channel),
what cannot be validated yet, and the implementation plan.

Signed-off-by: Yves Brissaud <yves@dagger.io>
The manifest-v2 entrypoint needs the module's type definitions as Dang source
at generate time, where today DaggerType only produces them as JavaPoet code
for the runtime register() call. Give DaggerType a second rendering backend so
both descriptions come from one model and cannot drift.

Signed-off-by: Yves Brissaud <yves@dagger.io>
Manifest v2 replaces the ambient FunctionCall entrypoint with a call() field
that hands a module its receiver, function name and arguments as JSON. Add the
process side of that protocol: one JSON request on stdin, one JSON result on
stdout.

The protocol is hand-written SDK code rather than generated text, so a
generated entrypoint only has to name its own dispatcher. Because call() has no
error field, a failure can only be a non-zero exit; the envelope written to
stderr is not part of the contract and nothing reads it, but it records what a
real error channel would have to carry.

Signed-off-by: Yves Brissaud <yves@dagger.io>
Manifest v2 has no ambient FunctionCall to read, so the dispatcher a module
generates has to be callable directly. Promote the private invoke method to a
public static daggerDispatch and give the generated main an engine-call mode
that drives it over stdin and stdout.

The v1 path is unchanged: dispatch(FunctionCall) still calls the same method,
and main with no arguments still runs the ambient entrypoint. The engine-call
branch exits before the telemetry block because there is no session to attach
spans to -- a regression the manifest v2 interface forces, recorded in the
design note.

Signed-off-by: Yves Brissaud <yves@dagger.io>
Manifest v2 asks a module for its type definitions as data, through a Dang
ModuleEntrypoint, instead of having the module register them over GraphQL at
run time. The annotation processor already holds the model that answers that:
give it a second backend that renders the same ModuleInfo as Dang.

The result is written as a resource rather than a source file, so it lands
beside the compiled classes instead of in the generated-sources tree that
becomes the module's committed Java.

Signed-off-by: Yves Brissaud <yves@dagger.io>
Add generateV2, which stages the Dang entrypoint the processor emits plus a
manifest-v2 body naming it. It is deliberately not a @generate function:
nothing can load either file yet, so running it as part of dagger generate
would only commit a dead entrypoint into every managed module.

The manifest goes to dagger-module.v2.toml rather than replacing
dagger-module.toml. This SDK reads a module's schema by asking the engine to
load it, and engine v1.0.0-beta.11 loads manifest v1 only, so replacing the
manifest would leave the module unloadable and generateV2 unrepeatable.

The dispatch check runs the generated jar in a plain Maven container that never
had a Dagger session, which proves the exported dispatcher answers a call
request without an engine.

Signed-off-by: Yves Brissaud <yves@dagger.io>
CI is green on the change it describes.

Signed-off-by: Yves Brissaud <yves@dagger.io>
@eunomie
eunomie force-pushed the java-sdk-manifest-v2-prototype-lead-c62ff1c1 branch from 227c047 to 12a2682 Compare September 3, 2026 15:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant